import { keepTrying } from '@/common'

export interface WsConnOptions {
  url: string
  maxErr: number
  pingInterval?: number
  onMessage(evt: string): void
  onConnectError(): void
}

enum WsConnState {
  CONNECTING = 0,
  CONNECTED = 1,
  DISCONNECTING = 2,
  DISCONNECTED = 3
}

export class WsConn {
  private sock: WebSocket | null = null
  private opts: WsConnOptions
  private pingTicker: number | null = null
  private errCnt = 0
  private _state: WsConnState = WsConnState.DISCONNECTED

  constructor(opts: WsConnOptions) {
    this.opts = opts
  }

  public get state(): WsConnState {
    return this._state
  }

  public async waitForConnected(timeout: number): Promise<any> {
    await keepTrying(() => {
      return this._state === WsConnState.CONNECTED
    }, timeout)
  }

  public start() {
    if (this._state !== WsConnState.DISCONNECTED) {
      return
    }
    this._state = WsConnState.CONNECTING
    this.connect()
  }

  public stop() {
    if (this._state !== WsConnState.CONNECTED && this._state !== WsConnState.CONNECTING) {
      return
    }
    this._state = WsConnState.DISCONNECTING
    this.closeWs()
  }

  private connect() {
    if (this.sock) {
      return
    }
    const sock = new WebSocket(this.opts.url)
    sock.onopen = () => {
      this.onWsOpen()
    }
    sock.onmessage = (evt) => {
      this.onWsMsg(evt)
    }
    sock.onclose = () => {
      this.onWsClose()
    }
    sock.onerror = () => {
      this.onWsError()
    }
    this.sock = sock
  }

  private closeWs() {
    if (this.sock) {
      this.sock.close()
    }
  }

  private stopPing() {
    if (this.pingTicker != null) {
      clearInterval(this.pingTicker)
      this.pingTicker = null
    }
  }

  private startPing() {
    this.stopPing()
    this.pingTicker = setInterval(() => {
      this.ping()
    }, this.opts.pingInterval || 20000)
  }

  private ping() {
    if (this.sock) {
      this.sock.send('ping')
    }
  }

  private onWsOpen() {
    this.startPing()
    this.errCnt = 0
    this._state = WsConnState.CONNECTED
  }

  private onWsClose() {
    this.stopPing()
    this.errCnt++
    this.sock = null
    if (this.errCnt > this.opts.maxErr || this._state === WsConnState.DISCONNECTING) {
      this._state = WsConnState.DISCONNECTED
      this.opts.onConnectError()
      return
    }
    this._state = WsConnState.CONNECTING
    setTimeout(() => {
      this.connect()
    }, 500)
  }

  private onWsError() {
    this.stopPing()
    this.errCnt++
    this.sock = null
    if (this.errCnt > this.opts.maxErr || this._state === WsConnState.DISCONNECTING) {
      this._state = WsConnState.DISCONNECTED
      this.opts.onConnectError()
      return
    }
    this._state = WsConnState.CONNECTING
    setTimeout(() => {
      this.connect()
    }, 500)
  }

  private onWsMsg(m: MessageEvent) {
    if (m.data === 'pong') {
      return
    }
    this.opts.onMessage(m.data)
  }
}
